/*
 * Copyright 2009 Tran Nhut Trung.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package com.gdevelop.gwtxp.rebind;


import com.gdevelop.gwtxp.client.GwtXP;
import com.gdevelop.gwtxp.client.GwtXPPages;
import com.gdevelop.gwtxp.rebind.el.ELParserContext;
import com.gdevelop.gwtxp.rebind.el.TypeIntrospector;
import com.gdevelop.gwtxp.rebind.gwt.GWTTagFactory;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.i18n.client.Constants;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.beans.PropertyChangeListener;

import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;

import org.eclipse.core.databinding.observable.gwt.GWTBeanObservableValue;


public class GwtXPGenerator extends Generator{
  static final String GWTXP_SUFFIX = "_GwtXP";
  private static final String SRC_DIR = "src";
  private static final String GWTXP_EXT = "gwtxp.xml";
  private static Vector<String> EXTENSIONS = new Vector<String>();
  static{
    EXTENSIONS.add(GWTXP_EXT);
  }

  static Map<String, TagFactory> tagFactories = new Hashtable<String, TagFactory>();
  static{
    tagFactories.put("http://www.gdevelop.com/gwtxp", new GWTTagFactory());
  }

  /**
   * The {@link TreeLogger} used to log messages.
   */
  private TreeLogger logger = null;
  private GeneratorContext context = null;
  private TypeOracle typeOracle;
  private Vector<File> gwtxpFiles = new Vector<File>();
  private Set<String> observableBeans = new HashSet<String>();

  public GwtXPGenerator() {
  }

  public String generate(TreeLogger logger, GeneratorContext context,
                         String typeName) throws UnableToCompleteException{
    long start = System.currentTimeMillis();
    this.logger = logger;
    this.context = context;
    this.typeOracle = context.getTypeOracle();
    
    gwtxpFiles.clear();
    observableBeans.clear();
    
    // Only generate files on the first permutation
    if (!isFirstPass(context, logger)) {
      return typeName + "Map";
    }

    JClassType type;
    try {
      type = typeOracle.getType(typeName);
    } catch (NotFoundException e) {
      return typeName + "Map";
    }

    try {
      GwtXPPages pages = type.getAnnotation(GwtXPPages.class);
      if (pages != null){
        for (String f : pages.value()){
          f = f.replace('.', File.separatorChar);
          gwtxpFiles.add(new File(SRC_DIR + File.separator + f + "." + GWTXP_EXT));
        }
      }else{
        // scanFiles(new File(SRC_DIR));
        String classpath = System.getProperty("java.class.path");
        StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
        while (st.hasMoreTokens()){
          String path = st.nextToken();
          if (path.endsWith(SRC_DIR)){
            File f = new File(path);
            if (f.isDirectory()){
              scanFiles(f);
            }
          }
        }
      }
      
      generateGwtXPMap(type);
      
      for (File f : gwtxpFiles){
        new GwtXPCreator(this, f).generate();
      }
      
      generateBeanObservableValueClasses();
    } catch (Exception e) {
      e.printStackTrace();
      logger.log(TreeLogger.ERROR, "Error wihie transforming gwtxp files", e);
      throw new UnableToCompleteException();
    }

    long end = System.currentTimeMillis();
    logger.log(TreeLogger.INFO, "Transforming " + gwtxpFiles.size() + " files in " + (end-start) + " ms.", null);
    
    return typeName + "Map";
  }

  /**
   * Add an observable bean name. 
   * This generator will generate ObservableValue or ObservableList for the bean
   * @param beanName
   */
  public void addObservableBean(String beanName){
    observableBeans.add(beanName);
  }
  
  public TreeLogger getLogger(){
    return logger;
  }
  public GeneratorContext getGeneratorContext(){
    return context;
  }
  
  /**
   * Ensure that we only generate files once by creating a placeholder file,
   * then looking for it on subsequent generates.
   * 
   * @return true if this is the first pass, false if not
   */
  private static boolean isFirstPass(GeneratorContext context, TreeLogger logger) {
    String placeholder = "generated";
    try {
      OutputStream outStream = context.tryCreateResource(logger, placeholder);
      if (outStream == null) {
        return false;
      } else {
        context.commitResource(logger, outStream);
      }
    } catch (UnableToCompleteException e) {
      logger.log(TreeLogger.ERROR, "Unable to generate", e);
      return false;
    }
    return true;
  }

  private void scanFiles(File base){
    Stack dirs = new Stack();
    dirs.push(base);
    while (!dirs.isEmpty()) {
      String s = dirs.pop().toString();
      File f = new File(s);
      if (f.exists() && f.isDirectory()) {
        String[] files = f.list();
        String ext;
        for (int i = 0; (files != null) && i < files.length; i++) {
          File f2 = new File(s, files[i]);
          if (f2.isDirectory()) {
            dirs.push(f2.getPath());
          } else {
            ext = files[i].substring(files[i].lastIndexOf('.') + 1);
            for (String e : EXTENSIONS){
              String file = files[i];
              if (file.endsWith(e)){
                gwtxpFiles.add(f2);
              }
            }
          }
        }
      }
    }
  }

  private void generateGwtXPMap(JClassType type) {
    String packageName = type.getPackage().getName();
    String simpleName = type.getSimpleSourceName() + "Map";
    ClassSourceFileComposerFactory composer = 
      new ClassSourceFileComposerFactory(packageName, simpleName);
    composer.setSuperclass("java.util.HashMap");
    composer.addImport(GWT.class.getName());
    composer.addImport(GwtXP.class.getName());
    PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);
    if (printWriter == null){
      return;
    }
    SourceWriter sw = composer.createSourceWriter(context, printWriter);
    
    sw.println("public " + simpleName + "(){");
    sw.indent();
    sw.println("GwtXP gwtXP = null;");
    for (File f : gwtxpFiles){
      String[] packageAndClassName = Utils.getPackageAndClassName(f);
      packageName = packageAndClassName[0];
      simpleName = packageAndClassName[1];
      String ext = packageAndClassName[2];
      
      String pageName;
      if ((packageName == null) || (packageName.length() == 0)){
        pageName = simpleName;
      }else{
        pageName = packageName + "." + simpleName;
      }
      // sw.println("this.put(\"" + pageName + "." + ext + "\", new " + pageName + GWTXP_SUFFIX + "());");
      sw.println();
      sw.println("gwtXP = new " + pageName + GWTXP_SUFFIX + "();");
      sw.println("this.put(\"" + pageName + "." + ext + "\", gwtXP);");
      sw.println("this.put(\"" + simpleName + "\", gwtXP);");
    }
    sw.outdent();
    sw.println("}");
    
    sw.commit(logger);
  }

  private void generateBeanObservableValueClasses() throws NotFoundException{
    JClassType typeString = typeOracle.getType(String.class.getName());
    JClassType typePropertyChangeListener = typeOracle.getType(PropertyChangeListener.class.getName());
    JClassType typeConstants = typeOracle.getType(Constants.class.getName());
    
    for (String beanClassName : observableBeans){
      JClassType beanType = typeOracle.getType(beanClassName);
      String packageName = beanType.getPackage().getName();
      String simpleName = beanType.getName() + "ObservableValue";

      ClassSourceFileComposerFactory composer = 
        new ClassSourceFileComposerFactory(packageName, simpleName);
      composer.setSuperclass(GWTBeanObservableValue.class.getName());
      composer.addImport(PropertyChangeListener.class.getName());
      composer.addImport("org.eclipse.core.databinding.observable.value.*");
      PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);
      if (printWriter == null){
        return;
      }
      SourceWriter out = composer.createSourceWriter(context, printWriter);

      out.println("public " + simpleName + "(IObservableValue prefixObservableValue, " + beanClassName + " bean, String propName){");
      out.indent();
      out.println("super(prefixObservableValue, bean, propName);");
      out.outdent();
      out.println("}");
      
      // Generate addPropertyChangeListener method
      out.println();
      out.println("public void addPropertyChangeListener(Object bean, String propName, PropertyChangeListener listener) {");
      out.indent();
      boolean hasAddPropertyChangeListener = false;
      try {
        if (ELParserContext.getMethod(beanType, "addPropertyChangeListener", new JType[]{typeString, typePropertyChangeListener}) != null){
          out.println("((" + beanClassName + ")bean).addPropertyChangeListener(propName, listener);");
          hasAddPropertyChangeListener = true;
        }
      } catch (NotFoundException e) {
        e.getMessage();
      }
      if (!hasAddPropertyChangeListener){
        try {
          if ((ELParserContext.getMethod(beanType, "addPropertyChangeListener", new JType[]{typePropertyChangeListener})) != null){
            out.println("((" + beanClassName + ")bean).addPropertyChangeListener(listener);");
          }
        } catch (NotFoundException e) {
           e.getMessage();
        }
      }
      out.outdent();
      out.println("}");
      
      // Generate removePropertyChangeListener method
      out.println();
      out.println("public void removePropertyChangeListener(Object bean, String propName, PropertyChangeListener listener) {");
      out.indent();
      boolean hasRemovePropertyChangeListener = false;
      try {
        if (ELParserContext.getMethod(beanType, "removePropertyChangeListener", new JType[]{typeString, typePropertyChangeListener}) != null){
          out.println("((" + beanClassName + ")bean).removePropertyChangeListener(propName, listener);");
          hasRemovePropertyChangeListener = true;
        }
      } catch (NotFoundException e) {
         e.getMessage();
      }
      if (!hasRemovePropertyChangeListener){
        try {
          if ((ELParserContext.getMethod(beanType, "removePropertyChangeListener", new JType[]{typePropertyChangeListener})) != null){
            out.println("((" + beanClassName + ")bean).removePropertyChangeListener(listener);");
          }
        } catch (NotFoundException e) {
           e.getMessage();
        }
      }
      out.outdent();
      out.println("}");

      // Generate getBeanProperty method
      boolean isConstant = typeConstants.isAssignableFrom(beanType);
      TypeIntrospector introspector = new TypeIntrospector(this.context);
      out.println();
      out.println("protected Object getBeanProperty(Object bean, String propName) {");
      out.indent();
      for (TypeIntrospector.PropertyDescriptor pd : introspector.getPropertyDescriptors(beanType)){
        if (pd.getReadMethod() == null){
          continue;
        }

        String propName = pd.getName();
        out.println("if (\"" + propName + "\".equals(propName))");
        out.indent();
        out.println("return ((" + beanType.getName() + ")bean)." + pd.getReadMethod().getName() + "();");
        out.outdent();
      }
      if (isConstant){
        for (JMethod m : beanType.getMethods()){
          if (m.getParameters().length != 0){
            continue;
          }
          String propName = m.getName();
          out.println("if (\"" + propName + "\".equals(propName)){");
          out.indent();
          out.println("return ((" + beanType.getName() + ")bean)." + propName + "();");
          out.outdent();
          out.println("}");
        }
      }
      out.println("return null;");
      out.outdent();
      out.println("}");

      // Generate getBeanProperty method
      out.println();
      out.println("protected void setBeanProperty(Object bean, String propName, Object value) {");
      out.indent();
      for (TypeIntrospector.PropertyDescriptor pd : introspector.getPropertyDescriptors(beanType)){
        if (pd.getWriteMethod() == null){
          continue;
        }
        String propName = pd.getName();
        out.println("if (\"" + propName + "\".equals(propName))");
        out.indent();
        out.println("((" + beanType.getName() + ")bean)." + ELParserContext.getSetMethodName(propName) + "((" + ELParserContext.autoboxed(typeOracle, pd.getWriteMethod().getParameters()[0].getType()).getQualifiedSourceName() +")value);");
        out.outdent();
      }
      out.outdent();
      out.println("}");

      out.println();
      out.println("protected Object getBeanPropertyType(Object bean, String propName) {");
      out.indent();
      for (TypeIntrospector.PropertyDescriptor pd : introspector.getPropertyDescriptors(beanType)){
        String propName = pd.getName();
        out.println("if (\"" + propName + "\".equals(propName))");
        out.indent();
        out.println("return " + ELParserContext.autoboxed(typeOracle, pd.getType()).getQualifiedSourceName() + ".class;");
        out.outdent();
      }
      if (isConstant){
        for (JMethod m : beanType.getMethods()){
          if (m.getParameters().length != 0){
            continue;
          }
          String propName = m.getName();
          out.println("if (\"" + propName + "\".equals(propName)){");
          out.indent();
          // out.println("Window.alert(\"Ah\");");
          out.println("return String.class;");
          out.outdent();
          out.println("}");
        }
      }
      out.println("return null;");
      out.outdent();
      out.println("}");

      out.commit(logger);
    }
  }
}
